package com.hmdp.controller;


import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSON;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.service.IShopService;
import com.hmdp.utils.CacheClient;
import com.hmdp.utils.RedisConstants;
import com.hmdp.utils.RedisData;
import com.hmdp.utils.SystemConstants;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 * 前端控制器
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@RestController
@RequestMapping("/shop")
public class ShopController {

    @Resource
    public IShopService shopService;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private CacheClient cacheClient;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);


    /**
     * 根据id查询商铺信息
     *
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        // 缓存穿透版
        // return getResult(id);

        // 缓存击穿:互斥锁
        // return getResultWithMutex(id);

        // 缓存击穿:逻辑过期
        // return getResultWithLogicalEx(id);

        // 工具类:缓存穿透
        Shop shop = cacheClient.getWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, shopService::getById,
            CACHE_SHOP_TTL, TimeUnit.SECONDS);

        // 工具类:逻辑过期
        // Shop shop = cacheClient.getWithLogicalEX(CACHE_SHOP_KEY, id, Shop.class, shopService::getById,
        //     CACHE_SHOP_TTL, TimeUnit.SECONDS);
        return Result.ok(shop);
    }

    private Result getResultWithLogicalEx(Long id) {
        Shop shop;
        // 1. 查询redis缓存
        String str_shop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        // 2. 判断是否存在，如果存在走缓存
        if (StrUtil.isNotBlank(str_shop)) {
            // 判断是否过期，如果没有过期直接返回
            RedisData redisData = JSONUtil.toBean(str_shop, RedisData.class);
            LocalDateTime expireTime = redisData.getExpireTime();
            JSONObject data = (JSONObject) redisData.getData();
            shop = JSONUtil.toBean(data, Shop.class);
            // 如果没有过期直接返回
            if (expireTime.isAfter(LocalDateTime.now())) {
                return Result.ok(shop);
            }
            // 如果过期了，并且获取到锁了，需要新开一个线程
            boolean isLock = tryLock(id);
            if (isLock) {
                System.out.println("reBuild" + isLock);
                CACHE_REBUILD_EXECUTOR.submit(() -> {
                    // 如果获取到了锁，进行重建
                    try {
                        getResultWithLogicalExSaveShop(id, 5L);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    } finally {
                        unLock(id);
                    }
                });
            }
            return Result.ok(shop);
        }
        return Result.ok("商品不存在");
    }

    public void getResultWithLogicalExSaveShop(Long id, Long expireSeconds) throws InterruptedException {
        // 1. 不存在从数据库查询
        Shop shop = shopService.getById(id);
        // 模拟延迟
        Thread.sleep(200);
        // 2. 在放到缓存中去，设置3秒后逻辑过期
        RedisData redisData = new RedisData();
        redisData.setExpireTime(LocalDate.now().atStartOfDay().plusSeconds(expireSeconds));
        redisData.setData(shop);
        String str_data = JSONUtil.toJsonStr(redisData);
        // 3. 写入Redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, str_data);
    }

    private Result getResultWithMutex(Long id) {
        Shop shop;
        // 1. 查询redis缓存
        String str_shop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        // 2. 判断是否存在，如果存在走缓存
        if (StrUtil.isNotBlank(str_shop)) {
            System.out.println("缓存击穿 go cache " + id);
            shop = JSONUtil.toBean(str_shop, Shop.class);
            return Result.ok(shop);
        }
        try {
            // 5. 如果不存在重建缓存
            boolean isLock = tryLock(id);
            if (!isLock) {
                // 6. 如果没有获取的锁，休息200ms重试
                Thread.sleep(200);
                getResultWithMutex(id);
            }

            // 3. 不存在从数据库查询
            shop = shopService.getById(id);
            if (shop == null) {
                return Result.fail("店铺不存在");
            }
            // 4. 在放到缓存中去
            str_shop = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, str_shop, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 7. 释放锁
            unLock(id);
        }
        return Result.ok(shop);
    }

    private boolean tryLock(Long shop_id) {
        Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(LOCK_SHOP_KEY + shop_id,
            "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(result);
    }

    private void unLock(Long shop_id) {
        stringRedisTemplate.delete(LOCK_SHOP_KEY + shop_id);
    }

    private Result getResult(Long id) {
        Shop shop;
        // 1. 查询redis缓存
        String str_shop = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        // 2. 判断是否存在，如果存在走缓存
        if (StrUtil.isNotBlank(str_shop)) {
            System.out.println("go cache " + id);
            shop = JSONUtil.toBean(str_shop, Shop.class);
            return Result.ok(shop);
        }
        // 6. 不存在，但是如果是空字符串，防止缓存穿透
        if ("".equals(str_shop)) {
            return Result.fail("店铺不存在");
        }

        // 3. 不存在从数据库查询
        shop = shopService.getById(id);
        if (shop == null) {
            // 5. 防止缓存穿透
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("店铺不存在");
        }
        // 4. 在放到缓存中去
        str_shop = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, str_shop, CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    /**
     * 新增商铺信息
     *
     * @param shop 商铺数据
     * @return 商铺id
     */
    @PostMapping
    public Result saveShop(@RequestBody Shop shop) {
        // 写入数据库
        shopService.save(shop);
        // 返回店铺id
        return Result.ok(shop.getId());
    }

    /**
     * 更新商铺信息
     *
     * @param shop 商铺数据
     * @return 无
     */
    @PutMapping
    public Result updateShop(@RequestBody Shop shop) {
        if (shop.getId() == null) {
            return Result.fail("店铺id不能为空");
        }
        shopService.updateShopById(shop);
        return Result.ok("更新成功");
    }

    /**
     * 根据商铺类型分页查询商铺信息
     *
     * @param typeId  商铺类型
     * @param current 页码
     * @return 商铺列表
     */
    @GetMapping("/of/type")
    public Result queryShopByType(
        @RequestParam("typeId") Integer typeId,
        @RequestParam(value = "current", defaultValue = "1") Integer current,
        @RequestParam(value = "x", required = false) Double x,
        @RequestParam(value = "y", required = false) Double y
    ) {
        return shopService.queryShopByType(typeId, current, x, y);
    }

    /**
     * 根据商铺名称关键字分页查询商铺信息
     *
     * @param name    商铺名称关键字
     * @param current 页码
     * @return 商铺列表
     */
    @GetMapping("/of/name")
    public Result queryShopByName(
        @RequestParam(value = "name", required = false) String name,
        @RequestParam(value = "current", defaultValue = "1") Integer current
    ) {
        // 根据类型分页查询
        Page<Shop> page = shopService.query()
            .like(StrUtil.isNotBlank(name), "name", name)
            .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 返回数据
        return Result.ok(page.getRecords());
    }
}
